package com.damagez.selfrefresh.redis;

import com.damagez.selfrefresh.annotation.RefreshCacheable;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author damage
 * @create 2020/5/9 16:52
 * @description
 */
@Slf4j
@Aspect
public class RefreshCacheCollectAspect implements BeanPostProcessor {

    public static final String BEAN_NAME = "refreshCacheCollectAspect";

    private KeyGenerator keyGenerator;

    public RefreshCacheCollectAspect(KeyGenerator keyGenerator) {
        this.keyGenerator = keyGenerator;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
        Map<Method, Cacheable> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                (MethodIntrospector.MetadataLookup<Cacheable>) method -> {
                    Cacheable annotation = AnnotatedElementUtils.getMergedAnnotation(method, Cacheable.class);
                    return annotation;
                });
        if (annotatedMethods.size() > 0){
            annotatedMethods.forEach((key,value) -> {
                for (final String cacheName : value.cacheNames()) {
                    //提前初始化，防止并发调用cacheToInvocationsMap时执行顺序不恰当导致出现竞态条件
                    cacheToInvocationsMap.put(cacheName, new CopyOnWriteArraySet<>());
                }
            });
        }
        return bean;
    }

    /**
     * Maintains Sets of CachedInvocation objects corresponding to each cache
     * configured in the application. At initialization, this map gets populated
     * with the cache name as the key and a hashSet as the value, for every
     * configured cache.
     */
    public static Map<String, Set<CachedInvocation>> cacheToInvocationsMap = new ConcurrentHashMap<>();

    /**
     * Avoid concurrent modification issues by using CopyOnWriteArraySet that
     * copies the internal array on every modification
     */
    private final Set<CachedInvocation> allInvocations = new CopyOnWriteArraySet<>();

    @Around("@annotation(refreshCacheable)")
    public Object refreshCache(ProceedingJoinPoint joinPoint, RefreshCacheable refreshCacheable) throws Throwable {
        Method annotatedElement = getSpecificMethod(joinPoint);
        Object key = keyGenerator.generate(joinPoint.getTarget(), annotatedElement, joinPoint.getArgs());
        final CachedInvocation invocation = new CachedInvocation(key, joinPoint.getTarget(), annotatedElement, joinPoint.getArgs());
        allInvocations.add(invocation);
        if (refreshCacheable.cacheNames().length == 0){
            if (log.isWarnEnabled()){
                String methodName = annotatedElement.getDeclaringClass().getName() + "." + annotatedElement.getName();
                log.warn("@RefreshCacheable can not get any cacheName which annotated on method :{},so cache no effect.",methodName);
            }
        }
        for (final String cacheName : refreshCacheable.cacheNames()) {
            cacheToInvocationsMap.get(cacheName).add(invocation);
        }
        return joinPoint.proceed();
    }

    /**
     * Finds out the most specific method when the execution reference is an
     * interface or a method with generic parameters
     *
     * @param pjp
     * @return
     */
    private Method getSpecificMethod(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        // The method may be on an interface, but we need attributes from the
        // target class. If the target class is null, the method will be
        // unchanged.
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget());
        if (targetClass == null && pjp.getTarget() != null) {
            targetClass = pjp.getTarget().getClass();
        }
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        // If we are dealing with method with generic parameters, find the
        // original method.
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
        return specificMethod;
    }

    protected static final class CachedInvocation{
        private Object key;
        private final Object targetBean;
        private final Method targetMethod;
        private Object[] arguments;
        private AtomicInteger invokeCount = new AtomicInteger(0);

        protected CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) {
            this.key = key;
            this.targetBean = targetBean;
            this.targetMethod = targetMethod;
            if (arguments != null && arguments.length != 0) {
                this.arguments = Arrays.copyOf(arguments, arguments.length);
                //  check if deep cloning is needed and implement
            }
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof CachedInvocation)) {
                return false;
            }
            final CachedInvocation other = (CachedInvocation) obj;
            return key.equals(other.getKey());
        }

        /**
         * @return the arguments
         */
        public Object[] getArguments() {
            return arguments;
        }

        /**
         * @return the targetBean
         */
        public Object getTargetBean() {
            return targetBean;
        }

        /**
         * @return the targetMethod
         */
        public Method getTargetMethod() {
            return targetMethod;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return key.hashCode();
        }

        public Object getKey() {
            return key;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "CachedInvocation [Key=" + key + ", targetBean=" + targetBean + ", targetMethod=" + targetMethod
                    + ", arguments=" + (arguments != null ? arguments.length : "none") + " ]";
        }

        public Integer increaseInvokeCount(){
            return invokeCount.getAndIncrement();
        }

        public void resetInvokeCount(){
            invokeCount.set(0);
        }

    }
}
